﻿// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Global

/* ImageImporterBmp.cs --
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using System;

using AM;

using PdfSharpCore.Pdf;
using PdfSharpCore.Pdf.Advanced;

#endregion

#nullable enable

namespace PdfSharpCore.Drawing.Internal;

// $THHO THHO4THHO add support for PdfDocument.Options.
internal class ImageImporterBmp
    : ImageImporterRoot, IImageImporter
{
    public ImportedImage? ImportImage
        (
            StreamReaderHelper stream,
            PdfDocument document
        )
    {
        try
        {
            stream.CurrentOffset = 0;
            if (TestBitmapFileHeader (stream, out var offsetImageData))
            {
                // Magic: TestBitmapFileHeader updates stream.CurrentOffset on success.

                var ipd = new ImagePrivateDataBitmap (stream.Data, stream.Length);
                ImportedImage ii = new ImportedImageBitmap (this, ipd, document);
                if (TestBitmapInfoHeader (stream, ii, offsetImageData))
                {
                    //stream.CurrentOffset = offsetImageData;
                    return ii;
                }
            }
        }

        // ReSharper disable once EmptyGeneralCatchClause
        catch (Exception)
        {
        }

        return null;
    }

    private bool TestBitmapFileHeader
        (
            StreamReaderHelper stream,
            out int offset
        )
    {
        offset = 0;

        // File must start with "BM".
        if (stream.GetWord (0, true) == 0x424d)
        {
            var filesize = (int)stream.GetDWord (2, false);

            // Integrity check: filesize set in BM header should match size of the stream.
            // We test "<" instead of "!=" to allow extra bytes at the end of the stream.
            if (filesize < stream.Length)
                return false;

            offset = (int)stream.GetDWord (10, false);
            stream.CurrentOffset += 14;
            return true;
        }

        return false;
    }

    private bool TestBitmapInfoHeader
        (
            StreamReaderHelper stream,
            ImportedImage importedImage,
            int offset
        )
    {
        var size = (int)stream.GetDWord (0, false);
        if (size == 40 || size == 108 ||
            size == 124) // sizeof BITMAPINFOHEADER == 40, sizeof BITMAPV4HEADER == 108, sizeof BITMAPV5HEADER == 124
        {
            var width = stream.GetDWord (4, false);
            var height = (int)stream.GetDWord (8, false);
            int planes = stream.GetWord (12, false);
            int bitcount = stream.GetWord (14, false);
            var compression = (int)stream.GetDWord (16, false);
            var sizeImage = (int)stream.GetDWord (20, false);
            var xPelsPerMeter = (int)stream.GetDWord (24, false);
            var yPelsPerMeter = (int)stream.GetDWord (28, false);
            var colorsUsed = stream.GetDWord (32, false);
            var colorsImportant = stream.GetDWord (36, false);
            colorsImportant.NotUsed();

            // TODO Integrity and plausibility checks.
            if (sizeImage != 0 && sizeImage + offset > stream.Length)
                return false;

            var privateData = (ImagePrivateDataBitmap)importedImage.Data;

            // Return true only for supported formats.
            if (compression == 0 || compression == 3) // BI_RGB == 0, BI_BITFIELDS == 3
            {
                ((ImagePrivateDataBitmap)importedImage.Data).Offset = offset;
                ((ImagePrivateDataBitmap)importedImage.Data).ColorPaletteOffset = stream.CurrentOffset + size;
                importedImage.Information.Width = width;
                importedImage.Information.Height = (uint)Math.Abs (height);
                importedImage.Information.HorizontalDPM = xPelsPerMeter;
                importedImage.Information.VerticalDPM = yPelsPerMeter;
                privateData.FlippedImage = height < 0;
                if (planes == 1 && bitcount == 24)
                {
                    // RGB24
                    importedImage.Information.ImageFormat = ImageInformation.ImageFormats.RGB24;

                    // TODO: Verify Mask if size >= 108 && compression == 3.
                    return true;
                }

                if (planes == 1 && bitcount == 32)
                {
                    // ARGB32
                    //ii.Information.ImageFormat = ImageInformation.ImageFormats.ARGB32;
                    importedImage.Information.ImageFormat = compression == 0
                        ? ImageInformation.ImageFormats.RGB24
                        : ImageInformation.ImageFormats.ARGB32;

                    // TODO: tell RGB from ARGB. Idea: assume RGB if alpha is always 0.

                    // TODO: Verify Mask if size >= 108 && compression == 3.
                    return true;
                }

                if (planes == 1 && bitcount == 8)
                {
                    // Palette8
                    importedImage.Information.ImageFormat = ImageInformation.ImageFormats.Palette8;
                    importedImage.Information.ColorsUsed = colorsUsed;

                    return true;
                }

                if (planes == 1 && bitcount == 4)
                {
                    // Palette8
                    importedImage.Information.ImageFormat = ImageInformation.ImageFormats.Palette4;
                    importedImage.Information.ColorsUsed = colorsUsed;

                    return true;
                }

                if (planes == 1 && bitcount == 1)
                {
                    // Palette8
                    importedImage.Information.ImageFormat = ImageInformation.ImageFormats.Palette1;
                    importedImage.Information.ColorsUsed = colorsUsed;

                    return true;
                }

                // TODO Implement more formats!
            }
        }

        return false;
    }


    public ImageData PrepareImage (ImagePrivateData data)
    {
        throw new NotImplementedException();
    }
}

/// <summary>
/// Bitmap refers to the format used in PDF. Will be used for BMP, PNG, TIFF, GIF and others.
/// </summary>
internal class ImportedImageBitmap : ImportedImage
{
    /// <summary>
    /// Initializes a new instance of the <see cref="ImportedImageBitmap"/> class.
    /// </summary>
    public ImportedImageBitmap (IImageImporter importer, ImagePrivateDataBitmap data, PdfDocument document)
        : base (importer, data, document)
    {
    }

    internal override ImageData PrepareImageData()
    {
        var data = (ImagePrivateDataBitmap)Data;
        var imageData = new ImageDataBitmap (_document);

        //imageData.Data = data.Data;
        //imageData.Length = data.Length;

        data.CopyBitmap (imageData);

        return imageData;
    }
}

// THHO4THHO Maybe there will be derived classes for direct bitmaps vs. palettized bitmaps or so. Time will tell.

/// <summary>
/// Contains data needed for PDF. Will be prepared when needed.
/// Bitmap refers to the format used in PDF. Will be used for BMP, PNG, TIFF, GIF and others.
/// </summary>
internal class ImageDataBitmap
    : ImageData
{
    internal ImageDataBitmap (PdfDocument document)
    {
        Data = null!;
        DataFax = null!;
        AlphaMask = null!;
        BitmapMask = null!;
        PaletteData = null!;

        _document = document;
    }

    /// <summary>
    /// Gets the data.
    /// </summary>
    public byte[] Data { get; internal set; }

    /// <summary>
    /// Gets the length.
    /// </summary>
    public int Length { get; internal set; }

    /// <summary>
    /// Gets the data.
    /// </summary>
    public byte[] DataFax { get; internal set; }

    /// <summary>
    /// Gets the length.
    /// </summary>
    public int LengthFax { get; internal set; }

    public byte[] AlphaMask { get; internal set; }

    public int AlphaMaskLength { get; internal set; }

    public byte[] BitmapMask { get; internal set; }

    public int BitmapMaskLength { get; internal set; }

    public byte[] PaletteData { get; set; }

    public int PaletteDataLength { get; set; }

    public bool SegmentedColorMask;

    public int IsBitonal;

    public int K;

    public bool IsGray;

    internal readonly PdfDocument _document;
}

/// <summary>
/// Image data needed for PDF bitmap images.
/// </summary>
internal class ImagePrivateDataBitmap : ImagePrivateData
{
    /// <summary>
    /// Initializes a new instance of the <see cref="ImagePrivateDataBitmap"/> class.
    /// </summary>
    public ImagePrivateDataBitmap (byte[] data, int length)
    {
        Data = data;
        Length = length;
    }

    /// <summary>
    /// Gets the data.
    /// </summary>
    public byte[] Data
    {
        get;

        //internal set { _data = value; }
    }

    /// <summary>
    /// Gets the length.
    /// </summary>
    public int Length
    {
        get;

        //internal set { _length = value; }
    }

    /// <summary>
    /// True if first line is the top line, false if first line is the bottom line of the image. When needed, lines will be reversed while converting data into PDF format.
    /// </summary>
    internal bool FlippedImage;

    /// <summary>
    /// The offset of the image data in Data.
    /// </summary>
    internal int Offset;

    /// <summary>
    /// The offset of the color palette in Data.
    /// </summary>
    internal int ColorPaletteOffset;

    internal void CopyBitmap (ImageDataBitmap dest)
    {
        switch (Image!.Information.ImageFormat)
        {
            case ImageInformation.ImageFormats.ARGB32:
                CopyTrueColorMemoryBitmap (3, 8, true, dest);
                break;

            case ImageInformation.ImageFormats.RGB24:
                CopyTrueColorMemoryBitmap (4, 8, false, dest);
                break;

            case ImageInformation.ImageFormats.Palette8:
                CopyIndexedMemoryBitmap (8, dest);
                break;

            case ImageInformation.ImageFormats.Palette4:
                CopyIndexedMemoryBitmap (4, dest);
                break;

            case ImageInformation.ImageFormats.Palette1:
                CopyIndexedMemoryBitmap (1, dest);
                break;


            default:
                throw new NotImplementedException();
        }
    }

    /// <summary>
    /// Copies images without color palette.
    /// </summary>
    /// <param name="components">4 (32bpp RGB), 3 (24bpp RGB, 32bpp ARGB)</param>
    /// <param name="bits">8</param>
    /// <param name="hasAlpha">true (ARGB), false (RGB)</param>
    /// <param name="dest">Destination </param>
    private void CopyTrueColorMemoryBitmap
        (
            int components,
            int bits,
            bool hasAlpha,
            ImageDataBitmap dest
        )
    {
        bits.NotUsed();

        var width = (int) Image!.Information.Width;
        var height = (int) Image.Information.Height;

        var logicalComponents = components;
        if (components == 4)
            logicalComponents = 3;

        var imageData = new byte[components * width * height];

        var hasMask = false;
        var hasAlphaMask = false;
        var alphaMask = hasAlpha ? new byte[width * height] : null;
        var mask = hasAlpha ? new MonochromeMask (width, height) : null;

        var nFileOffset = Offset;
        var nOffsetRead = 0;
        if (logicalComponents == 3)
        {
            for (var y = 0; y < height; ++y)
            {
                // TODO Handle Flipped.
                var nOffsetWrite = 3 * (height - 1 - y) * width;
                var nOffsetWriteAlpha = 0;
                if (hasAlpha)
                {
                    mask!.StartLine (y);
                    nOffsetWriteAlpha = (height - 1 - y) * width;
                }

                for (var x = 0; x < width; ++x)
                {
                    imageData[nOffsetWrite] = Data[nFileOffset + nOffsetRead + 2];
                    imageData[nOffsetWrite + 1] = Data[nFileOffset + nOffsetRead + 1];
                    imageData[nOffsetWrite + 2] = Data[nFileOffset + nOffsetRead];
                    if (hasAlpha)
                    {
                        mask!.AddPel (Data[nFileOffset + nOffsetRead + 3]);
                        alphaMask![nOffsetWriteAlpha] = Data[nFileOffset + nOffsetRead + 3];
                        if (!hasMask || !hasAlphaMask)
                        {
                            if (Data[nFileOffset + nOffsetRead + 3] != 255)
                            {
                                hasMask = true;
                                if (Data[nFileOffset + nOffsetRead + 3] != 0)
                                    hasAlphaMask = true;
                            }
                        }

                        ++nOffsetWriteAlpha;
                    }

                    nOffsetRead += hasAlpha ? 4 : components;
                    nOffsetWrite += 3;
                }

                nOffsetRead = 4 * ((nOffsetRead + 3) / 4); // Align to 32 bit boundary
            }
        }
        else if (components == 1)
        {
            // Grayscale
            throw new NotImplementedException ("Image format not supported (grayscale).");
        }

        dest.Data = imageData;
        dest.Length = imageData.Length;

        if (alphaMask != null)
        {
            dest.AlphaMask = alphaMask;
            dest.AlphaMaskLength = alphaMask.Length;
        }

        if (mask != null)
        {
            dest.BitmapMask = mask.MaskData;
            dest.BitmapMaskLength = mask.MaskData.Length;
        }
    }

    private void CopyIndexedMemoryBitmap (int bits /*, ref bool hasAlpha*/, ImageDataBitmap dest)
    {
        int firstMaskColor = -1, lastMaskColor = -1;
        var segmentedColorMask = false;

        var bytesColorPaletteOffset =
            ((ImagePrivateDataBitmap) Image!.Data)
            .ColorPaletteOffset; // GDI+ always returns Windows bitmaps: sizeof BITMAPFILEHEADER + sizeof BITMAPINFOHEADER

        var bytesFileOffset = ((ImagePrivateDataBitmap)Image.Data).Offset;
        var paletteColors = Image.Information.ColorsUsed;
        var width = (int)Image.Information.Width;
        var height = (int)Image.Information.Height;

        var mask = new MonochromeMask (width, height);

        var isGray = bits == 8 && (paletteColors == 256 || paletteColors == 0);
        var isBitonal = 0; // 0: false; >0: true; <0: true (inverted)
        var paletteData = new byte[3 * paletteColors];
        for (var color = 0; color < paletteColors; ++color)
        {
            paletteData[3 * color] = Data[bytesColorPaletteOffset + 4 * color + 2];
            paletteData[3 * color + 1] = Data[bytesColorPaletteOffset + 4 * color + 1];
            paletteData[3 * color + 2] = Data[bytesColorPaletteOffset + 4 * color + 0];
            if (isGray)
                isGray = paletteData[3 * color] == paletteData[3 * color + 1] &&
                         paletteData[3 * color] == paletteData[3 * color + 2];

            if (Data[bytesColorPaletteOffset + 4 * color + 3] < 128)
            {
                // We treat this as transparency:
                if (firstMaskColor == -1)
                    firstMaskColor = color;
                if (lastMaskColor == -1 || lastMaskColor == color - 1)
                    lastMaskColor = color;
                if (lastMaskColor != color)
                    segmentedColorMask = true;
            }

            //else
            //{
            //  // We treat this as opacity:
            //}
        }

        if (bits == 1)
        {
            if (paletteColors == 0)
                isBitonal = 1;
            if (paletteColors == 2)
            {
                if (paletteData[0] == 0 &&
                    paletteData[1] == 0 &&
                    paletteData[2] == 0 &&
                    paletteData[3] == 255 &&
                    paletteData[4] == 255 &&
                    paletteData[5] == 255)
                    isBitonal = 1; // Black on white
                if (paletteData[5] == 0 &&
                    paletteData[4] == 0 &&
                    paletteData[3] == 0 &&
                    paletteData[2] == 255 &&
                    paletteData[1] == 255 &&
                    paletteData[0] == 255)
                    isBitonal = -1; // White on black
            }
        }

        // NYI: (no sample found where this was required)
        // if (segmentedColorMask = true)
        // { ... }

        var imageData = new byte[((width * bits + 7) / 8) * height];
        byte[]? imageDataFax = null;
        var k = 0;


        if (bits == 1 && dest._document.Options.EnableCcittCompressionForBilevelImages)
        {
            // TODO: flag/option?
            // We try Group 3 1D and Group 4 (2D) encoding here and keep the smaller byte array.
            //byte[] temp = new byte[imageData.Length];
            //int ccittSize = DoFaxEncoding(ref temp, imageBits, (uint)bytesFileOffset, (uint)width, (uint)height);

            // It seems that Group 3 2D encoding never beats both other encodings, therefore we don't call it here.
            //byte[] temp2D = new byte[imageData.Length];
            //uint dpiY = (uint)image.VerticalResolution;
            //uint kTmp = 0;
            //int ccittSize2D = DoFaxEncoding2D((uint)bytesFileOffset, ref temp2D, imageBits, (uint)width, (uint)height, dpiY, out kTmp);
            //k = (int) kTmp;

            var tempG4 = new byte[imageData.Length];
            var ccittSizeG4 =
                PdfImage.DoFaxEncodingGroup4 (ref tempG4, Data, (uint)bytesFileOffset, (uint)width, (uint)height);

            var isFaxEncoding = ccittSizeG4 > 0;
            if (isFaxEncoding)
            {
                //if (ccittSize == 0)
                //  ccittSize = 0x7fffffff;
                if (ccittSizeG4 == 0)
                    ccittSizeG4 = 0x7fffffff;

                //if (ccittSize <= ccittSizeG4)
                //{
                //  Array.Resize(ref temp, ccittSize);
                //  imageDataFax = temp;
                //  k = 0;
                //}
                //else
                {
                    Array.Resize (ref tempG4, ccittSizeG4);
                    imageDataFax = tempG4;
                    k = -1;
                }
            }
        }

        //if (!isFaxEncoding)
        {
            var bytesOffsetRead = 0;
            if (bits == 8 || bits == 4 || bits == 1)
            {
                var bytesPerLine = (width * bits + 7) / 8;
                for (var y = 0; y < height; ++y)
                {
                    mask.StartLine (y);
                    var bytesOffsetWrite = (height - 1 - y) * ((width * bits + 7) / 8);
                    for (var x = 0; x < bytesPerLine; ++x)
                    {
                        if (isGray)
                        {
                            // Lookup the gray value from the palette:
                            imageData[bytesOffsetWrite] = paletteData[3 * Data[bytesFileOffset + bytesOffsetRead]];
                        }
                        else
                        {
                            // Store the palette index.
                            imageData[bytesOffsetWrite] = Data[bytesFileOffset + bytesOffsetRead];
                        }

                        if (firstMaskColor != -1)
                        {
                            int n = Data[bytesFileOffset + bytesOffsetRead];
                            if (bits == 8)
                            {
                                // TODO???: segmentedColorMask == true => bad mask NYI
                                mask.AddPel ((n >= firstMaskColor) && (n <= lastMaskColor));
                            }
                            else if (bits == 4)
                            {
                                // TODO???: segmentedColorMask == true => bad mask NYI
                                var n1 = (n & 0xf0) / 16;
                                var n2 = (n & 0x0f);
                                mask.AddPel ((n1 >= firstMaskColor) && (n1 <= lastMaskColor));
                                mask.AddPel ((n2 >= firstMaskColor) && (n2 <= lastMaskColor));
                            }
                            else if (bits == 1)
                            {
                                // TODO???: segmentedColorMask == true => bad mask NYI
                                for (var bit = 1; bit <= 8; ++bit)
                                {
                                    var n1 = (n & 0x80) / 128;
                                    mask.AddPel ((n1 >= firstMaskColor) && (n1 <= lastMaskColor));
                                    n *= 2;
                                }
                            }
                        }

                        bytesOffsetRead += 1;
                        bytesOffsetWrite += 1;
                    }

                    bytesOffsetRead = 4 * ((bytesOffsetRead + 3) / 4); // Align to 32 bit boundary
                }
            }
            else
            {
                throw new NotImplementedException ("ReadIndexedMemoryBitmap: unsupported format #3");
            }
        }

        dest.Data = imageData;
        dest.Length = imageData.Length;

        if (imageDataFax != null)
        {
            dest.DataFax = imageDataFax;
            dest.LengthFax = imageDataFax.Length;
        }

        dest.IsGray = isGray;
        dest.K = k;
        dest.IsBitonal = isBitonal;

        dest.PaletteData = paletteData;
        dest.PaletteDataLength = paletteData.Length;
        dest.SegmentedColorMask = segmentedColorMask;

        //if (alphaMask != null)
        //{
        //    dest.AlphaMask = alphaMask;
        //    dest.AlphaMaskLength = alphaMask.Length;
        //}

        if (mask != null! && firstMaskColor != -1)
        {
            dest.BitmapMask = mask.MaskData;
            dest.BitmapMaskLength = mask.MaskData.Length;
        }
    }
}
